Photo by Riccardo Pelati on Unsplash
Character Animation
The critical point of animating a small object is “animate the motion individually with another canvas or SVG,” especially when the objective is running with a slower or higher FPS. No matter which method you choose, after the animation is created, we could use the canvas context mothed “drawImage” to embed the image into the game.
Sprite
Prepare a sprite sheet. The figure below is a sprite that shows a character running from left to right.
Next, we need the width of each posture and measure the x-axis to reach the frame. Generally speaking, each frame would have the same width.
Frame 1:
width: 80
height: 86
x-axis: 0
Frame 2:
width: 80
height: 86
x-axis: 80
Frame 3:
width: 80
height: 86
x-axis: 160
Sprite Animate Factory
I created an example on the codesandbox website.
In the beginning, there is a canvas with the id “runningRight” in HTML.
<canvas id="runningRight"></canvas>
In JavaScript, we get the canvas node by “getElementById.”
const nodeRunning = document.getElementById("runningRight");
Next, I created a factory to generate the motion.
class PlayerAction {
constructor(args){
}
init = () => {
}
render = () => {
}
}
There are two functions inside the class besides the constructor. The init function prepares the sprite sheet and the canvas context. The render function calls itself continuously to draw the sequent frame.
The init function
Some data should be saved in the factory, the sprite image, the width and height of each frame, etc.
constructor(args) {
img: null,
imgSrc: "",
width: 80,
height: 86,
// ...
}
In the init function, I kept the canvas context and the sprite sheet image. After everything was prepared, I called the window.requestAnimationFrame
to perform the animation.
init = () => {
// set width and height of canvas
this.node.width = this.width;
this.node.height = this.height;
// get the canvas context
this.ctx = this.node.getContext("2d");
// keep the image
this.img = new Image();
this.img.src = this.imgSrc;
// start render
requestAnimationFrame(this.render);
};
The render function
drawImage()
is a canvas API that can be used to do dynamic image compositing. It lets us cut out a section of the source image, then scale and draw it on our canvas.
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
The code below shows how I sliced the first frame of the sprite sheet.
ctx.drawImage(
this.img, // image
0, // source image sx
0, // source image sy
this.width, // source image width: 80
this.height, // source image height: 86
0, // destination canvas dx
0, // destination canvas dy
this.width, // destination canvas width: 80
this.height // destination canvas height: 86
);
As I mentioned above, the sprite sheet distance from the left to the target frame should be stored. I kept the distance in the constructor.
The current frame should be saved, as well. The sequent posture would be rendered by looping from frame one to frame six infinitely.
constructor(args){
// ...
// the array of source image sx
posIndexes: [0, 80, 160, 240, 320, 400],
// the next frame would be currentPos++
currentPos: 0,
// ...
}
render = () => {
ctx.drawImage(
this.img,
posIndexes[currentPos],
0,
this.width,
this.height,
0,
0,
this.width,
this.height
);
// the next frame
this.currentPos++;
const lastPos = posIndexes.length - 1;
if (currentPos === lastPos) {
// if the last last frame is done, start from the first frame
this.currentPos = 0;
}
requestAnimationFrame(this.render);
}
Do not forget to clear the previous frame before drawing the next frame.
// clear previous image
ctx.clearRect(0, 0, width, height);
ctx.drawImage(
...
);
Thus, we would get the following code to draw all movements.
render = () => {
const {
ctx,
width,
height,
img,
posIndexes,
currentPos
} = this;
ctx.drawImage(
img,
posIndexes[currentPos],
0,
width,
height,
0,
0,
width,
height
);
// the next frame
this.currentPos++;
const lastPos = posIndexes.length - 1;
if (currentPos === lastPos) {
// if the last last frame is done, start from the first frame
this.currentPos = 0;
}
requestAnimationFrame(this.render);
}
However, you might find out that the character is running with insane speed. We need to slow it down naturally.
There are many ways to defer the animation. The way I have done was to set a counter. If the count was less than a certain number, then returned. Else the counter would be reset to zero.
constructor(args){
// ...,
frameCount: 0,
frameCountDelay: 8
}
render = () => {
// slow down the running speed
this.frameCount++;
if (frameCount < frameCountDelay) {
requestAnimationFrame(this.render);
return;
}
this.frameCount = 0;
}
So far, the single-movement animation finished. The full code follows.
Embed
The following will focus on how to embed the running movement into the game.
In the previous chapter, I provided the code of rendering different movements to the corresponding situations.
class Player {
init = () => {
// init player actions
}
update = () => {
// leA
}
render = () => {
// draw different movements
if (isHurt) {
// hurt
this.hurt();
return;
}
if (!isStepOnStair) {
// jump
this.jump();
} else {
if (isRunning) {
// run
this.run();
} else {
// stand
this.stand();
}
}
}
}
At first, I created movements by the PlayerAction()
custructor. Characters had different actions, such as jumping and running. So I added a running-right movement to each character.
const characterOne = {};
characterOne.runningRight = document.createElement("canvas");
const runningRightAction = new PlayerAction({
node: characterOne.runningRight
imgSrc: require("@/assets/player1/runningRight.png")
});
runningRightAction.init();
Similarly, a running-left movement should be added as well. You can flip the sprite sheet horizontally by any of the online tools.
characterOne.runningLeft = document.createElement("canvas");
const runningLeftAction = new PlayerAction({
node: characterOne.runningLeft
imgSrc: require("@/assets/player1/runningLeft.png")
});
runningLeftAction.init();
After generating the movements, let us back to the class Player.
Assume that the player is now running on the stair. Through the “if/else” condition, the execution will dive into this.run()
function.
run = () => {
let running = direction ? characterOne.runningRight : characterOne.runningLeft;
ctx.drawImage(
running,
0,
0,
width,
height,
destination.x,
destination.y,
width,
height
);
}
As the code above, the function drawImage could be spotted quickly. This canvas API can use any of the following data types as an image source.
- Any
<img>
Element as well as those images created by theImage()
custructor. - SVG
<image>
Element - A frame grabbed from
<video>
Element - Another
<canvas>
Element
Yes, we can embed another <canvas>
image into the current canvas. Therefore, we can easily use the drawImage()
function to lay the character movement on the destination we want.
Summary
The canvas APIdrawImage()
is a useful tool to create canvas animation. It can generate a sprite sheet animation and frame images on composite canvas.
Creating the movement individually with another canvas helps us to manage multiple actions easier.